Skip to content

Conversation

@jjhembd
Copy link
Contributor

@jjhembd jjhembd commented Oct 9, 2025

Description

This PR adds experimental support for loading 3D Tiles as terrain, via Cesium3DTilesTerrainProvider.

What 3D Tiles data can be used?

3D Tiles loaded with Cesium3DTilesTerrainProvider must follow this structure (thanks @lilleyse for the list):

  • Conforms to the same WGS84 double-headed quadtree tiling scheme as quantized mesh. Must have two root tiles with region bounding volumes and implicit tiling.
  • Multiple contents are not allowed
  • Tile metadata must include TILE_MINIMUM_HEIGHT, TILE_MAXIMUM_HEIGHT, TILE_BOUNDING_SPHERE, TILE_HORIZON_OCCLUSION_POINT semantics
  • Only GLB is supported, not glTF
  • Each GLB must have a single node, mesh, and primitive
  • Primitive must have POSITION attribute
  • Primitive must have NORMAL attribute if requestVertexNormals is true
  • Primitive must have indices with UNSIGNED_SHORT or UNSIGNED_INT component type
  • Attributes and indices must be tightly packed
  • If requestWaterMask is true, primitive must reference an EXT_structural_metadata property texture with WATERMASK semantic.
  • Primitive must have CESIUM_tile_edges extension
  • Model may be compressed with EXT_meshopt_compression / KHR_mesh_quantization

Issue number and link

Resolves #12296

Testing plan

TODO before moving this out of "Draft":

  • Update Sandcastles to new gallery format
  • Open separate PR for doc fixes, Check calls: EllipsoidalOccluder, GoogleEarthEnterpriseTerrainData, TerrainEncoding (separate from other changes in file), binarySearch, mergeSort, QuantizedMeshTerrainDataSpec. Beware incorrect capitalizations like {Object}, {Number[]}! See Clean up docs and type checks for terrain providers #12969
  • Verify changes to ResourceCache, avoid additional URI parsing.
  • Replace promises in specs with async/await
  • Consider dropping the duplicate ImplicitSubtreeCache in Cesium3DTilesTerrainProvider.js (replace with ImplicitSubtreeCache.js) The two versions of ImplicitSubtreeCache have some key differences, so I think a refactor is out of scope for this PR.
  • Understand changes to signature in TerrainEncoding

Author checklist

  • I have submitted a Contributor License Agreement
  • I have added my name to CONTRIBUTORS.md
  • I have updated CHANGES.md with a short summary of my change
  • I have added or updated unit tests to ensure consistent code coverage
  • I have updated the inline documentation, and included code examples where relevant
  • I have performed a self-review of my code

@jjhembd jjhembd changed the base branch from main to terrain-docs October 10, 2025 22:05
@jjhembd jjhembd changed the base branch from terrain-docs to main October 10, 2025 22:31
@jjhembd jjhembd changed the base branch from main to terrain-docs October 10, 2025 22:32
Base automatically changed from terrain-docs to main October 16, 2025 21:14
@jjhembd jjhembd marked this pull request as ready for review October 23, 2025 22:32
@jjhembd jjhembd requested a review from jjspace October 27, 2025 15:19
Copy link
Contributor

@jjspace jjspace left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @jjhembd, overall this looks pretty good to me!

I mostly reviewed this from a docs perspective. I believe most of this has already been verified and sitting around for a while. I also tested the sandcastle some but probably not exhaustive. I think it could be good to have a second perspective on the actual logic

Comment on lines 97 to 100
options = options ?? Frozen.EMPTY_OBJECT;

//>>includeStart('debug', pragmas.debug);
if (!defined(options) || !defined(options.buffer)) {
throw new DeveloperError("options.buffer is required.");
}
if (!defined(options.width)) {
throw new DeveloperError("options.width is required.");
}
if (!defined(options.height)) {
throw new DeveloperError("options.height is required.");
}
Check.defined("options.buffer", options.buffer);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Edit: NO change requested right now, just commenting for discussion. Probably best as it's own issue

I think this is equivalent to:

//>>includeStart('debug', pragmas.debug);
Check.defined("options", options);
Check.defined("options.buffer", options.buffer);

Or maybe even better Check.typeOf.object("options", options)

I started to call this out in other places but realized we actually use this pattern a lot and it's probably not worth addressing in this PR.

Specifically I think it's an issue when options is not marked as optional but the ?? makes it behave as if it is. Using this will punt potential errors down the call chain to somewhere where it can't work with undefined. I think it would be better if this function itself throws errors because it can't access values of undefined.

* @param {number} options.skirtHeight The height of the skirt to add on the edges of the tile.
* @param {Credit[]} [options.credits] Array of credits for this tile.
*/
function Cesium3DTilesUpsampleTerrainData(options) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this class be in it's own file? no strong preference I think, just asking

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cesium3DTilesUpsampleTerrainData and its "parent" Cesium3DTilesTerrainData share the helper functions interpolateMeshHeight and upsampleMesh.

To separate the classes while avoiding circular dependencies, I suppose we could make those helpers static methods of Cesium3DTilesUpsampleTerrainData. This structure makes sense in the case of upsampleMesh, which always returns a Cesium3DTilesUpsampleTerrainData, but feels a little forced in the case of interpolateMeshHeight.

The other issue is that both the helpers have very long function signatures, which is tolerable (maybe?) for an internal helper, but doesn't make for a very pretty (or maintainable or testable) API for a class method.

I don't love the current structure but I think a refactor might be too much for this PR.

Comment on lines +3 to +7
const terrainProvider =
await Cesium.Cesium3DTilesTerrainProvider.fromIonAssetId(3923568, {
requestVertexNormals: true, // Needed for hillshade lighting
requestWaterMask: true, // Needed to distinguish land from water
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sandcastle essentially has only 1 change compared to the "Globe materials - Water mask" sandcastle. That's the terrain provider. Would it be better to combine them and have a dropdown or something to toggle which terrain provider is being used? Or is there a better, more unique, way to demonstrate that this is using 3D Tiles as terrain?

Copy link
Contributor Author

@jjhembd jjhembd Oct 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated the description in the .yaml file to emphasize the 3D Tiles as terrain aspect.
I agree there's still a lot of overlap. I can combine them if you prefer, though I think I would set the 3D Tiles data as the starting point, just to make sure we capture that dataset & code path during end-to-end testing or manual release checks.

_y,
_level,
) {
return undefined;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this function supposed to be "unimplemented"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question... Only CesiumTerrainProvider implements this function.
EllipsoidTerrainProvider, VRTheWorldTerrainProvider, GoogleEarthEnterpriseTerrainProvider, and ArcGISTiledElevationTerrainProvider all return undefined too.

I haven't traced through how it's used, but this looks like an interface feature that isn't absolutely necessary for terrains to work.

@jjhembd
Copy link
Contributor Author

jjhembd commented Oct 31, 2025

Thanks for the feedback @jjspace. I think I addressed it where I could, and left comments where the fix is not obvious or maybe out of scope. Let me know if I'm missing anything, or if there's a good way to clean up that I didn't think of.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bring Cesium3DTilesTerrainProvider into main

8 participants